﻿using FortuneArterial.Site.BusinessLogicLayer.LunceneSearch.Interface;
using FortuneArterial.Site.Model;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Lucene.Net.Store;
using System;
using System.Collections.Generic;
using System.Linq;

namespace FortuneArterial.Site.BusinessLogicLayer.LunceneSearch.Service
{
    public class LuceneService
       : LuceneService<BaseEntity, StandardAnalyzer>
    {
        public const Lucene.Net.Util.Version LUCENE_VERSION = Lucene.Net.Util.Version.LUCENE_30;
    }

    public class LuceneService<T>
        : LuceneService<T, StandardAnalyzer>
        where T : BaseEntity, new()
    {
    }

    public class LuceneService<T, K>
        where T : BaseEntity, new()
        where K: Analyzer
    {
        #region CONST & variables

        private string _luceneDir;

        #endregion

        #region ctor

        public LuceneService()
            : this(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "lucene_index"))
        {

        }

        public LuceneService(string luceneDir)
        {
            _luceneDir = System.IO.Path.GetFullPath(luceneDir);
            // create default Lucene search index directory
            if (!System.IO.Directory.Exists(_luceneDir))
                System.IO.Directory.CreateDirectory(_luceneDir);
        }

        #endregion

        #region properties

        private FSDirectory _directory;
        private FSDirectory Directory
        {
            get
            {
                if (_directory == null) _directory = FSDirectory.Open(new System.IO.DirectoryInfo(_luceneDir));
                if (IndexWriter.IsLocked(_directory)) IndexWriter.Unlock(_directory);
                var lockFilePath = System.IO.Path.Combine(_luceneDir, "write.lock");
                if (System.IO.File.Exists(lockFilePath)) System.IO.File.Delete(lockFilePath);
                return _directory;
            }
        }

        #endregion

        private Analyzer _createAnalyzer() {
            var ctors = typeof(K).GetConstructors();
            if (ctors.Any(m => !m.GetParameters().Any()))
            {
                return ctors.First(m => !m.GetParameters().Any()).Invoke(null) as K;
            }
            else {
                return ctors.First().Invoke(new object[] { LuceneService.LUCENE_VERSION }) as K;
            }
        }

        // search methods
        public IEnumerable<T> GetAllIndexRecords()
        {
            // validate search index
            if (!System.IO.Directory.EnumerateFiles(_luceneDir).Any()) return new List<T>();

            // set up lucene searcher
            var searcher = new IndexSearcher(this.Directory, false);
            var reader = IndexReader.Open(this.Directory, false);
            var docs = new List<Document>();
            var term = reader.TermDocs();
            // v 2.9.4: use 'hit.Doc()'
            // v 3.0.3: use 'hit.Doc'
            while (term.Next()) docs.Add(searcher.Doc(term.Doc));
            reader.Dispose();
            searcher.Dispose();
            return _mapLuceneToDataList(docs);
        }

        public IEnumerable<T> SearchDefault(string input, string fieldName = "")
        {
            return string.IsNullOrEmpty(input) ? new List<T>() : _search(input, fieldName);
        }

        // main search method
        private IEnumerable<T> _search(string searchQuery, string searchField = "")
        {
            // validation
            if (string.IsNullOrEmpty(searchQuery.Replace("*", "").Replace("?", ""))) return new List<T>();

            // set up lucene searcher
            using (var searcher = new IndexSearcher(this.Directory, false))
            {
                var hits_limit = 1000;
                var analyzer = _createAnalyzer();
                ScoreDoc[] hits;

                // search by single field
                if (!string.IsNullOrEmpty(searchField))
                {
                    var parser = new QueryParser
                        (LuceneService.LUCENE_VERSION, searchField, analyzer);
                    var query = parseQuery(searchQuery, parser);
                    hits = searcher.Search(query, hits_limit).ScoreDocs;
                }
                // search by multiple fields (ordered by RELEVANCE)
                else
                {
                    var parser = new MultiFieldQueryParser
                        (LuceneService.LUCENE_VERSION, typeof(T).GetProperties().Select(m => m.Name).ToArray(), analyzer);
                    var query = parseQuery(searchQuery, parser);
                    hits = searcher.Search(query, null, hits_limit, Sort.INDEXORDER).ScoreDocs;
                }

                var results = _mapLuceneToDataList(hits, searcher);
                analyzer.Close();
                searcher.Dispose();
                return results;
            }
        }

        private Query parseQuery(string searchQuery, QueryParser parser)
        {
            Query query;
            try
            {
                query = parser.Parse(searchQuery.Trim());
            }
            catch (ParseException)
            {
                query = parser.Parse(QueryParser.Escape(searchQuery.Trim()));
            }
            return query;
        }

        // map Lucene search index to data
        private IEnumerable<T> _mapLuceneToDataList(IEnumerable<Document> hits)
        {
            return hits.Select(_mapLuceneDocumentToData).ToList();
        }

        private IEnumerable<T> _mapLuceneToDataList(IEnumerable<ScoreDoc> hits, IndexSearcher searcher)
        {
            // v 2.9.4: use 'hit.doc'
            // v 3.0.3: use 'hit.Doc'
            return hits.Select(hit => _mapLuceneDocumentToData(searcher.Doc(hit.Doc))).ToList();
        }

        private T _mapLuceneDocumentToData(Document doc)
        {
            T model = new T();

            try
            {
                (model as ILuceneEntity).FromDocument(doc);
            }
            catch
            {
                foreach (var p in typeof(T).GetProperties())
                {
                    if (p.CanWrite)
                    {
                        p.SetValue(model, doc.Get(p.Name));
                    }
                }
            }
            

            return model;
        }

        // add/update/clear search index data 
        public void AddUpdateLuceneIndex(T model)
        {
            AddUpdateLuceneIndex(new List<T> {model});
        }

        public void AddUpdateLuceneIndex(IEnumerable<T> models)
        {
            // init lucene
            var analyzer = _createAnalyzer();
            using (var writer = new IndexWriter(this.Directory, analyzer, IndexWriter.MaxFieldLength.UNLIMITED))
            {
                // add data to lucene search index (replaces older entries if any)
                foreach (var model in models) _addToLuceneIndex(model, writer);

                // close handles
                analyzer.Close();
                writer.Dispose();
            }
        }

        public void ClearLuceneIndexRecord(string recordId)
        {
            // init lucene
            var analyzer = _createAnalyzer();
            using (var writer = new IndexWriter(this.Directory, analyzer, IndexWriter.MaxFieldLength.UNLIMITED))
            {
                // remove older index entry
                var searchQuery = new TermQuery(new Term("ID", recordId));
                writer.DeleteDocuments(searchQuery);

                // close handles
                analyzer.Close();
                writer.Dispose();
            }
        }

        public bool ClearLuceneIndex()
        {
            try
            {
                var analyzer = _createAnalyzer();
                using (
                    var writer = new IndexWriter(this.Directory, analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED))
                {
                    // remove older index entries
                    writer.DeleteAll();

                    // close handles
                    analyzer.Close();
                    writer.Dispose();
                }
            }
            catch (Exception)
            {
                return false;
            }
            return true;
        }

        public void Optimize()
        {
            var analyzer = _createAnalyzer();
            using (var writer = new IndexWriter(this.Directory, analyzer, IndexWriter.MaxFieldLength.UNLIMITED))
            {
                analyzer.Close();
                writer.Optimize();
                writer.Dispose();
            }
        }

        private void _addToLuceneIndex(T model, IndexWriter writer)
        {
            // remove older index entry
            var searchQuery = new TermQuery(new Term("ID", model.ID));
            writer.DeleteDocuments(searchQuery);

            // add new index entry
            var doc = new Document();
            // add lucene fields mapped to db fields
            doc.Add(new Field("ID", model.ID, Field.Store.YES, Field.Index.NOT_ANALYZED));

            foreach (var p in typeof(T).GetProperties())
            {
                if (p.CanRead && p.Name != "ID")
                {
                    doc.Add(new Field(
                        p.Name,
                        p.GetValue(model) == null ? "" : p.GetValue(model).ToString(),
                        Field.Store.YES,
                        Field.Index.ANALYZED
                        ));
                }
            }

            // add entry to index
            writer.AddDocument(doc);
        }

        private void _addToLuceneIndex(ILuceneEntity model, IndexWriter writer)
        {
            // remove older index entry
            var searchQuery = new TermQuery(new Term("ID", model.ID));
            writer.DeleteDocuments(searchQuery);

            // add new index entry
            var doc = (model as ILuceneEntity).ToDocument();

            // add entry to index
            writer.AddDocument(doc);
        }
    }
}